/*
 * Copyright (C) 2007-2013 Geometer Plus <contact@geometerplus.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package org.geometerplus.fbreader.fbreader;

import java.util.Collections;
import java.util.Date;
import java.util.List;

import org.geometerplus.fbreader.book.Book;
import org.geometerplus.fbreader.book.BookEvent;
import org.geometerplus.fbreader.book.BookUtil;
import org.geometerplus.fbreader.book.Bookmark;
import org.geometerplus.fbreader.book.BookmarkQuery;
import org.geometerplus.fbreader.book.IBookCollection;
import org.geometerplus.fbreader.bookmodel.BookModel;
import org.geometerplus.fbreader.bookmodel.BookReadingException;
import org.geometerplus.fbreader.bookmodel.TOCTree;
import org.geometerplus.fbreader.fbreader.options.CancelMenuHelper;
import org.geometerplus.fbreader.fbreader.options.FooterOptions;
import org.geometerplus.fbreader.fbreader.options.PageTurningOptions;
import org.geometerplus.fbreader.fbreader.options.ViewOptions;
import org.geometerplus.zlibrary.core.application.ZLApplication;
import org.geometerplus.zlibrary.core.application.ZLKeyBindings;
import org.geometerplus.zlibrary.core.options.ZLBooleanOption;
import org.geometerplus.zlibrary.core.options.ZLColorOption;
import org.geometerplus.zlibrary.core.options.ZLEnumOption;
import org.geometerplus.zlibrary.core.options.ZLIntegerRangeOption;
import org.geometerplus.zlibrary.core.options.ZLStringOption;
import org.geometerplus.zlibrary.core.util.MiscUtil;
import org.geometerplus.zlibrary.core.util.ZLColor;
import org.geometerplus.zlibrary.text.hyphenation.ZLTextHyphenator;
import org.geometerplus.zlibrary.text.model.ZLTextModel;
import org.geometerplus.zlibrary.text.view.ZLTextFixedPosition;
import org.geometerplus.zlibrary.text.view.ZLTextPosition;
import org.geometerplus.zlibrary.text.view.ZLTextView;
import org.geometerplus.zlibrary.text.view.ZLTextWordCursor;
import org.geometerplus.zlibrary.text.view.style.ZLTextStyleCollection;

public final class FBReaderApp extends ZLApplication
{

	private boolean mGotoPosition = true;

	public final ZLTextStyleCollection TextStyleCollection = new ZLTextStyleCollection("Base");

	public final ZLBooleanOption AllowScreenBrightnessAdjustmentOption = new ZLBooleanOption("LookNFeel",
			"AllowScreenBrightnessAdjustment", true);
	public final ZLStringOption TextSearchPatternOption = new ZLStringOption("TextSearch", "Pattern", "");

	public final ZLBooleanOption EnableDoubleTapOption = new ZLBooleanOption("Options", "EnableDoubleTap", false);
	public final ZLBooleanOption NavigateAllWordsOption = new ZLBooleanOption("Options", "NavigateAllWords", false);

	public static enum WordTappingAction
	{
		doNothing, selectSingleWord, startSelecting, openDictionary
	}

	public final ZLEnumOption<WordTappingAction> WordTappingActionOption = new ZLEnumOption<WordTappingAction>("Options",
			"WordTappingAction", WordTappingAction.startSelecting);

	public final ZLColorOption ImageViewBackgroundOption = new ZLColorOption("Colors", "ImageViewBackground", new ZLColor(255, 255, 255));
	public final ZLEnumOption<FBView.ImageFitting> FitImagesToScreenOption = new ZLEnumOption<FBView.ImageFitting>("Options",
			"FitImagesToScreen", FBView.ImageFitting.covers);

	public static enum ImageTappingAction
	{
		doNothing, selectImage, openImageView
	}

	public final ZLEnumOption<ImageTappingAction> ImageTappingActionOption = new ZLEnumOption<ImageTappingAction>("Options",
			"ImageTappingAction", ImageTappingAction.openImageView);

	public final ViewOptions ViewOptions = new ViewOptions();

	public final ZLIntegerRangeOption ScrollbarTypeOption = new ZLIntegerRangeOption("Options", "ScrollbarType", 0, 3,
			FBView.SCROLLBAR_SHOW_AS_FOOTER);

	final ZLStringOption ColorProfileOption = new ZLStringOption("Options", "ColorProfile", ColorProfile.DAY);

	public final PageTurningOptions PageTurningOptions = new PageTurningOptions();
	public final FooterOptions FooterOptions = new FooterOptions();

	private final ZLKeyBindings myBindings = new ZLKeyBindings("Keys");

	public final FBView BookTextView;
	public final FBView FootnoteView;
	private String myFootnoteModelId;

	public volatile BookModel Model;

	private ZLTextPosition myJumpEndPosition;
	private Date myJumpTimeStamp;

	public final IBookCollection Collection;

	public FBReaderApp(IBookCollection collection)
	{
		Collection = collection;

		collection.addListener(new IBookCollection.Listener()
		{
			public void onBookEvent(BookEvent event, Book book)
			{
				switch (event)
				{
				case BookmarkStyleChanged:
				case BookmarksUpdated:
					if (Model != null && (book == null || book.equals(Model.Book)))
					{
						if (BookTextView.getModel() != null)
						{
							setBookmarkHighlightings(BookTextView, null);
						}
						if (FootnoteView.getModel() != null && myFootnoteModelId != null)
						{
							setBookmarkHighlightings(FootnoteView, myFootnoteModelId);
						}
					}
					break;
				case Updated:
					onBookUpdated(book);
					break;
				}
			}

			public void onBuildEvent(IBookCollection.Status status)
			{}
		});

		addAction(ActionCode.INCREASE_FONT, new ChangeFontSizeAction(this, +2));
		addAction(ActionCode.DECREASE_FONT, new ChangeFontSizeAction(this, -2));

		addAction(ActionCode.FIND_NEXT, new FindNextAction(this));
		addAction(ActionCode.FIND_PREVIOUS, new FindPreviousAction(this));
		addAction(ActionCode.CLEAR_FIND_RESULTS, new ClearFindResultsAction(this));

		addAction(ActionCode.SELECTION_CLEAR, new SelectionClearAction(this));

		addAction(ActionCode.TURN_PAGE_FORWARD, new TurnPageAction(this, true));
		addAction(ActionCode.TURN_PAGE_BACK, new TurnPageAction(this, false));

		addAction(ActionCode.MOVE_CURSOR_UP, new MoveCursorAction(this, FBView.Direction.up));
		addAction(ActionCode.MOVE_CURSOR_DOWN, new MoveCursorAction(this, FBView.Direction.down));
		addAction(ActionCode.MOVE_CURSOR_LEFT, new MoveCursorAction(this, FBView.Direction.rightToLeft));
		addAction(ActionCode.MOVE_CURSOR_RIGHT, new MoveCursorAction(this, FBView.Direction.leftToRight));

		addAction(ActionCode.VOLUME_KEY_SCROLL_FORWARD, new VolumeKeyTurnPageAction(this, true));
		addAction(ActionCode.VOLUME_KEY_SCROLL_BACK, new VolumeKeyTurnPageAction(this, false));

		addAction(ActionCode.SWITCH_TO_DAY_PROFILE, new SwitchProfileAction(this, ColorProfile.DAY));
		addAction(ActionCode.SWITCH_TO_NIGHT_PROFILE, new SwitchProfileAction(this, ColorProfile.NIGHT));

		addAction(ActionCode.EXIT, new ExitAction(this));

		BookTextView = new FBView(this);
		FootnoteView = new FBView(this);

		setView(BookTextView);
	}

	public void openBook(final Book book, final Bookmark bookmark, final Runnable postAction, boolean gotoPosition)
	{
		mGotoPosition = gotoPosition;

		if (book != null || Model == null)
		{
			runWithMessage("loadingBook", new Runnable()
			{
				public void run()
				{
					openBookInternal(book, bookmark, false);
					if (book != null)
					{
						book.addLabel(Book.READ_LABEL);
						Collection.saveBook(book);
					}
				}
			}, postAction);
		}
	}

	public void reloadBook()
	{
		if (Model != null && Model.Book != null)
		{
			runWithMessage("loadingBook", new Runnable()
			{
				public void run()
				{
					openBookInternal(Model.Book, null, true);
				}
			}, null);
		}
	}

	private ColorProfile myColorProfile;

	public ColorProfile getColorProfile()
	{
		if (myColorProfile == null)
		{
			myColorProfile = ColorProfile.get(getColorProfileName());
		}
		return myColorProfile;
	}

	public String getColorProfileName()
	{
		return ColorProfileOption.getValue();
	}

	public void setColorProfileName(String name)
	{
		ColorProfileOption.setValue(name);
		myColorProfile = null;
	}

	public ZLKeyBindings keyBindings()
	{
		return myBindings;
	}

	public FBView getTextView()
	{
		return (FBView) getCurrentView();
	}

	public void tryOpenFootnote(String id)
	{
		if (Model != null)
		{
			myJumpEndPosition = null;
			myJumpTimeStamp = null;
			BookModel.Label label = Model.getLabel(id);
			if (label != null)
			{
				if (label.ModelId == null)
				{
					if (getTextView() == BookTextView)
					{
						addInvisibleBookmark();
						myJumpEndPosition = new ZLTextFixedPosition(label.ParagraphIndex, 0, 0);
						myJumpTimeStamp = new Date();
					}
					BookTextView.gotoPosition(label.ParagraphIndex, 0, 0);
					setView(BookTextView);
				}
				else
				{
					setFootnoteModel(label.ModelId);
					setView(FootnoteView);
					FootnoteView.gotoPosition(label.ParagraphIndex, 0, 0);
				}
				getViewWidget().repaint();
			}
		}
	}

	public void clearTextCaches()
	{
		BookTextView.clearCaches();
		FootnoteView.clearCaches();
	}

	public Bookmark addSelectionBookmark()
	{
		final FBView fbView = getTextView();
		final String text = fbView.getSelectedText();

		final Bookmark bookmark = new Bookmark(Model.Book, fbView.getModel().getId(), fbView.getSelectionStartPosition(),
				fbView.getSelectionEndPosition(), text, true);
		Collection.saveBookmark(bookmark);
		fbView.clearSelection();

		return bookmark;
	}

	private void setBookmarkHighlightings(ZLTextView view, String modelId)
	{
		view.removeHighlightings(BookmarkHighlighting.class);
		for (BookmarkQuery query = new BookmarkQuery(Model.Book, 20);; query = query.next())
		{
			final List<Bookmark> bookmarks = Collection.bookmarks(query);
			if (bookmarks.isEmpty())
			{
				break;
			}
			for (Bookmark b : bookmarks)
			{
				if (b.getEnd() == null)
				{
					b.findEnd(view);
				}
				if (MiscUtil.equals(modelId, b.ModelId))
				{
					view.addHighlighting(new BookmarkHighlighting(view, Collection, b));
				}
			}
		}
	}

	private void setFootnoteModel(String modelId)
	{
		final ZLTextModel model = Model.getFootnoteModel(modelId);
		FootnoteView.setModel(model);
		if (model != null)
		{
			myFootnoteModelId = modelId;
			setBookmarkHighlightings(FootnoteView, modelId);
		}
	}

	synchronized void openBookInternal(Book book, Bookmark bookmark, boolean force)
	{
		if (book == null)
		{
			book = Collection.getRecentBook(0);
			if (book == null || !book.File.exists())
			{
				book = Collection.getBookByFile(BookUtil.getHelpFile());
			}
			if (book == null) { return; }
			book.addLabel(Book.READ_LABEL);
			Collection.saveBook(book);
		}
		if (mGotoPosition && !force && Model != null && book.equals(Model.Book))
		{
			if (bookmark != null)
			{
				gotoBookmark(bookmark, false);
			}
			return;
		}

		onViewChanged();

		storePosition();
		BookTextView.setModel(null);
		FootnoteView.setModel(null);
		clearTextCaches();

		Model = null;
		System.gc();
		System.gc();
		try
		{
			// TODO 关键所在
			Model = BookModel.createModel(book);
			

			Collection.saveBook(book);
			ZLTextHyphenator.Instance().load(book.getLanguage());
			BookTextView.setModel(Model.getTextModel());
			setBookmarkHighlightings(BookTextView, null);

			if (mGotoPosition) BookTextView.gotoPosition(Collection.getStoredPosition(book.getId()));
			if (bookmark == null) setView(BookTextView);
			else gotoBookmark(bookmark, false);

			Collection.addBookToRecentList(book);

			// final StringBuilder title = new StringBuilder(book.getTitle());
			// if (!book.authors().isEmpty()) {
			// boolean first = true;
			// for (Author a : book.authors()) {
			// title.append(first ? " (" : ", ");
			// title.append(a.DisplayName);
			// first = false;
			// }
			// title.append(")");
			// }
			// setTitle(title.toString());
		} catch (BookReadingException e)
		{
			processException(e);
		}

		getViewWidget().reset();
		getViewWidget().repaint();
	}

	private List<Bookmark> invisibleBookmarks()
	{
		final List<Bookmark> bookmarks = Collection.bookmarks(new BookmarkQuery(Model.Book, false, 10));
		Collections.sort(bookmarks, new Bookmark.ByTimeComparator());
		return bookmarks;
	}

	public boolean jumpBack()
	{
		try
		{
			if (getTextView() != BookTextView)
			{
				showBookTextView();
				return true;
			}

			if (myJumpEndPosition == null || myJumpTimeStamp == null) { return false; }
			// more than 2 minutes ago
			if (myJumpTimeStamp.getTime() + 2 * 60 * 1000 < new Date().getTime()) { return false; }
			if (!myJumpEndPosition.equals(BookTextView.getStartCursor())) { return false; }

			final List<Bookmark> bookmarks = invisibleBookmarks();
			if (bookmarks.isEmpty()) { return false; }
			final Bookmark b = bookmarks.get(0);
			Collection.deleteBookmark(b);
			gotoBookmark(b, true);
			return true;
		} finally
		{
			myJumpEndPosition = null;
			myJumpTimeStamp = null;
		}
	}

	private void gotoBookmark(Bookmark bookmark, boolean exactly)
	{
		final String modelId = bookmark.ModelId;
		if (modelId == null)
		{
			addInvisibleBookmark();
			if (exactly)
			{
				BookTextView.gotoPosition(bookmark);
			}
			else
			{
				BookTextView.gotoHighlighting(new BookmarkHighlighting(BookTextView, Collection, bookmark));
			}
			setView(BookTextView);
		}
		else
		{
			setFootnoteModel(modelId);
			if (exactly)
			{
				FootnoteView.gotoPosition(bookmark);
			}
			else
			{
				FootnoteView.gotoHighlighting(new BookmarkHighlighting(FootnoteView, Collection, bookmark));
			}
			setView(FootnoteView);
		}
		getViewWidget().repaint();
	}

	public void showBookTextView()
	{
		setView(BookTextView);
	}

	public void onWindowClosing()
	{
		storePosition();
	}

	public void storePosition()
	{
		if (Model != null && Model.Book != null && BookTextView != null)
		{
			Collection.storePosition(Model.Book.getId(), BookTextView.getStartCursor());
			Model.Book.setProgress(BookTextView.getProgress());
			Collection.saveBook(Model.Book);
		}
	}

	public boolean hasCancelActions()
	{
		return new CancelMenuHelper().getActionsList(Collection).size() > 1;
	}

	public void runCancelAction(CancelMenuHelper.ActionType type, Bookmark bookmark)
	{
		switch (type)
		{
		// case library:
		// runAction(ActionCode.SHOW_LIBRARY);
		// break;
		// case networkLibrary:
		// runAction(ActionCode.SHOW_NETWORK_LIBRARY);
		// break;
		case previousBook:
			openBook(Collection.getRecentBook(1), null, null, true);
			break;
		case returnTo:
			Collection.deleteBookmark(bookmark);
			gotoBookmark(bookmark, true);
			break;
		case close:
			closeWindow();
			break;
		}
	}

	private synchronized void updateInvisibleBookmarksList(Bookmark b)
	{
		if (Model != null && Model.Book != null && b != null)
		{
			for (Bookmark bm : invisibleBookmarks())
			{
				if (b.equals(bm))
				{
					Collection.deleteBookmark(bm);
				}
			}
			Collection.saveBookmark(b);
			final List<Bookmark> bookmarks = invisibleBookmarks();
			for (int i = 3; i < bookmarks.size(); ++i)
			{
				Collection.deleteBookmark(bookmarks.get(i));
			}
		}
	}

	public void addInvisibleBookmark(ZLTextWordCursor cursor)
	{
		if (cursor != null && Model != null && Model.Book != null && getTextView() == BookTextView)
		{
			updateInvisibleBookmarksList(Bookmark.createBookmark(Model.Book, getTextView().getModel().getId(), cursor, 6, false));
		}
	}

	public void addInvisibleBookmark()
	{
		if (Model.Book != null && getTextView() == BookTextView)
		{
			updateInvisibleBookmarksList(createBookmark(6, false));
		}
	}

	public Bookmark createBookmark(int maxLength, boolean visible)
	{
		final FBView view = getTextView();
		final ZLTextWordCursor cursor = view.getStartCursor();

		if (cursor.isNull()) { return null; }

		return Bookmark.createBookmark(Model.Book, view.getModel().getId(), cursor, maxLength, visible);
	}

	public TOCTree getCurrentTOCElement()
	{
		final ZLTextWordCursor cursor = BookTextView.getStartCursor();
		if (Model == null || cursor == null) { return null; }

		int index = cursor.getParagraphIndex();
		if (cursor.isEndOfParagraph())
		{
			++index;
		}
		TOCTree treeToSelect = null;
		for (TOCTree tree : Model.TOCTree)
		{
			final TOCTree.Reference reference = tree.getReference();
			if (reference == null)
			{
				continue;
			}
			if (reference.ParagraphIndex > index)
			{
				break;
			}
			treeToSelect = tree;
		}
		return treeToSelect;
	}

	public void onBookUpdated(Book book)
	{
		if (Model == null || Model.Book == null || !Model.Book.equals(book)) { return; }

		final String newEncoding = book.getEncodingNoDetection();
		final String oldEncoding = Model.Book.getEncodingNoDetection();

		Model.Book.updateFrom(book);

		if (newEncoding != null && !newEncoding.equals(oldEncoding))
		{
			reloadBook();
		}
		else
		{
			ZLTextHyphenator.Instance().load(Model.Book.getLanguage());
			clearTextCaches();
			getViewWidget().repaint();
		}
	}
}
